package com.java.base.io;

import org.junit.Test;

/**
 * <h1>IO优化</h1>
 * <h2>I/O与CPU时间的比较</h2>
 * <p>
 * I/O 操作比在内存中进行数据处理任务所需时间更长，差别要以数量级计。许多程序员一门心思扑在他们的对象
 * 如何加工数据上，对影响数据读取和存储的环境问题却不屑一顾。
 * <p>
 * ./image/interview.png 为数据单元进行磁盘读写所需时间的假设值
 *
 * <ul>
 * <li>第一列为处理一个数据单元所需平均时间</li>
 * <li>第二列为对该数据单元进行磁盘读写所需时间</li>
 * <li>第三列为每秒所能处理的数据单元数</li>
 * <li>第四列为改变第一第二列的值所能产生的数据吞吐率的提升值</li>
 * </ul>
 * 前三行显示了处理阶段的效率提升会如何影响吞吐率。把单位处理时间减半，仅能提高吞吐率2.2％。而另一方面，
 * 仅仅缩短 I/O 延迟 10％，就可使吞吐率增加 9.7％；把 I/O 时间减半，吞吐率几乎翻番。当您了解到 I/O
 * 花在一个数据单元上的时间是处理时间的 20 倍，这样的结果就不足为奇了。表中所列并非真实数据，目的只在说
 * 明相对时间度量，现实情况绝非如此简单。正如您所看到的，影响应用程序执行效率的限定性因素，往往并非处理速
 * 率，而是 I/O。
 *
 * <h2>用户空间与内核空间</h2>
 * <p>
 * 现在操作系统都是采用虚拟存储器，那么对32位操作系统而言，它的寻址空间（虚拟存储空间）为4G（2的32次方）。
 * 操作系统的核心是内核，独立于普通的应用程序，可以访问受保护的内存空间，也有访问底层硬件设备的所有权限。为
 * 了保证用户进程不能直接操作内核（kernel），保证内核的安全，操心系统将虚拟空间划分为两部分，一部分为内核
 * 空间，一部分为用户空间。针对linux操作系统而言，将最高的1G字节（从虚拟地址0xC0000000到0xFFFFFFFF），
 * 供内核使用，称为内核空间，而将较低的3G字节（从虚拟地址0x00000000到0xBFFFFFFF），供各个进程使用，称为
 * 用户空间。
 *
 * <h2>缓冲区操作</h2>
 * <p>
 * 缓冲区，以及缓冲区如何工作，是所有 I/O 的基础。所谓“输入／输出”讲的无非就是把数据移进或移出缓冲区。进程
 * 执行 I/O 操作，归结起来，也就是向操作系统发出请求，让它要么把缓冲区里的数据排干（写），要么用数据把缓冲区
 * 填满（读）。进程使用这一机制处理所有数据进出操作。操作系统内部处理这一任务的机制，其复杂程度可能超乎想像，
 * 但就概念而言，却非常直白易懂。
 * <p>
 * I/O 缓冲区操作简图 ./image/io2.png
 * <p>
 * 图中明显忽略了很多细节，仅显示了涉及到的基本步骤。
 * <p>
 * ./image/io2.png简单描述了数据从外部磁盘向运行中的进程的内存区域移动的过程。进程使用 read( )系统调用，
 * 要求其缓冲区被填满。内核随即向磁盘控制硬件发出命令，要求其从磁盘读取数据。磁盘控制器把数据直接写入内核内存
 * 缓冲区，这一步通过 DMA 完成，无需主 CPU 协助。一旦磁盘控制器把缓冲区装满，内核即把数据从内核空间的临时缓
 * 冲区拷贝到进程执行 read( )调用时指定的缓冲区。
 * <p>
 * 注意图中用户空间和内核空间的概念。用户空间是常规进程所在区域。 JVM 就是常规进程，驻守于用户空间。用户空间是
 * 非特权区域：比如，在该区域执行的代码就不能直接访问硬件设备。内核空间是操作系统所在区域。内核代码有特别的权力：
 * 它能与设备控制器通讯，控制着用户区域进程的运行状态，等等。最重要的是，所有 I/O 都直接（如这里所述）或间接通
 * 过内核空间。
 * <p>
 * 当进程请求 I/O 操作的时候，它执行一个系统调用（有时称为陷阱）将控制权移交给内核。
 * <p>
 * C/C++程序员所熟知的底层函数 open( )、 read( )、 write( )和 close( )要做的无非就是建立和执行适当的系统
 * 调用。当内核以这种方式被调用，它随即采取任何必要步骤，找到进程所需数据，并把数据传送到用户空间内的指定缓冲区。
 * 内核试图对数据进行高速缓存或预读取，因此进程所需数据可能已经在内核空间里了。如果是这样，该数据只需简单地拷贝出来
 * 即可。如果数据不在内核空间，则进程被挂起，内核着手把数据读进内存。
 * <p>
 * 看了./image/io2.png，您可能会觉得，把数据从内核空间拷贝到用户空间似乎有些多余。为什么不直接让磁盘控制器把数据
 * 送到用户空间的缓冲区呢？这样做有几个问题。
 *
 * <ul>
 * <li>首先，硬件通常不能直接访问用户空间 。</li>
 * <li>其次，像磁盘这样基于块存储的硬件设备操作的是固定大小的数据块，而用户进程请求的可能是任意大小的或非对齐的数据块。</li>
 * <li>在数据往来于用户空间与存储设备的过程中，内核负责数据的分解、再组合工作，因此充当着中间人的角色。</li>
 * </ul>
 * <h2>发散／汇聚</h2>
 * 许多操作系统能把组装／分解过程进行得更加高效。根据发散／汇聚的概念，进程只需一个系统调用，就能把一连串缓冲区
 * 地址传递给操作系统。然后，内核就可以顺序填充或排干多个缓冲区，读的时候就把数据发散到多个用户空间缓冲区，写的
 * 时候再从多个缓冲区把数据汇聚起来，
 * <p>
 * ./image/io3.png 三个缓冲区的发散操作
 * <p>
 * 这样用户进程就不必多次执行系统调用（那样做可能代价不菲），内核也可以优化数据的处理过程，因为它已掌握待传输数据
 * 的全部信息。如果系统配有多个 CPU，甚至可以同时填充或排干多个缓冲区
 *
 * @author xuweizhi
 * @date 2019/03/20 9:42
 */
public class IoOptimization {

    /**
     * <h2>虚拟内存</h2>
     * 所有现代操作系统都使用虚拟内存。虚拟内存意为使用虚假（或虚拟）地址取代物理（硬件RAM）内存地址。这样做好处
     * 颇多，总结起来可分为两大类：
     * <ul>
     * <li>一个以上的虚拟地址可指向同一个物理内存地址。</li>
     * <li>虚拟内存空间可大于实际可用的硬件内存。</li>
     * </ul>
     * <p>
     * 前面提到，设备控制器不能通过 DMA 直接存储到用户空间，但通过利用上面提到的第一项，则可以达到相同效果。把内核
     * 空间地址与用户空间的虚拟地址映射到同一个物理地址，这样，DMA 硬件（只能访问物理内存地址）就可以填充对内核与用
     * 户空间进程同时可见的缓冲区，图./image/io4.png
     *
     * <p>
     * 这样省去了内核与用户空间的往来拷贝，但前提条件是，内核与用户缓冲区必须使用相同的页对齐，缓冲区的大小还必须是磁
     * 盘控制器块大小（通常为 512 字节磁盘扇区）的倍数。操作系统把内存地址空间划分为页，即固定大小的字节组。内存页的
     * 大小总是磁盘块大小的倍数，通常为 2 次幂（这样可简化寻址操作）。典型的内存页为 1,024、 2,048 和 4,096 字节。
     * 虚拟和物理内存页的大小总是相同的。./image/io5.png 显示了来自多个虚拟地址的虚拟内存页是如何映射到物理内的。
     *
     * <h2>内存页面调度</h2>
     * 为了支持虚拟内存的第二个特性（寻址空间大于物理内存），就必须进行虚拟内存分页（经常
     * 称为交换，虽然真正的交换是在进程层面完成，而非页层面）。依照该方案，虚拟内存空间的页面能够继续存在于外部磁盘存储，
     * 这样就为物理内存中的其他虚拟页面腾出了空间。从本质上说，物理内存充当了分页区的高速缓存；而所谓分页区，即从物理内
     * 存置换出来，转而存储于磁盘上的内存页面。
     *
     * <p>
     * ./image/io6.png 显示了分属于四个进程的虚拟页面，其中每个进程都有属于自己的虚拟内存空间。进程A 有五个页面，其
     * 中两个装入内存，其余存储于磁盘。
     *
     * <p>
     * ./image/io6.png 用于分页区高速缓存的物理内存
     *
     * <p>
     * 把内存页大小设定为磁盘块大小的倍数，这样内核就可直接向磁盘控制硬件发布命令，把内存页写入磁盘，在需要时再重新装入。
     * 结果是，所有磁盘 I/O 都在页层面完成。对于采用分页技术的现代操作系统而言，这也是数据在磁盘与物理内存之间往来的唯一
     * 方式。
     *
     * <p>
     * 现代 CPU 包含一个称为内存管理单元（MMU）的子系统，逻辑上位于 CPU 与物理内存之间。该设备包含虚拟地址向物理内存地址
     * 转换时所需映射信息。当 CPU 引用某内存地址时， MMU负责确定该地址所在页（往往通过对地址值进行移位或屏蔽位操作实现），
     * 并将虚拟页号转换为物理页号（这一步由硬件完成，速度极快）。如果当前不存在与该虚拟页形成有效映射的物理内存页， MMU 会
     * 向 CPU 提交一个页错误。
     * <p>
     * 页错误随即产生一个陷阱（类似于系统调用），把控制权移交给内核，附带导致错误的虚拟地址信息，然后内核采取步骤验证页的
     * 有效性。内核会安排页面调入操作，把缺失的页内容读回物理内存。这往往导致别的页被移出物理内存，好给新来的页让地方。在
     * 这种情况下，如果待移出的页已经被碰过了（自创建或上次页面调入以来，内容已发生改变），还必须首先执行页面调出，把页内
     * 容拷贝到磁盘上的分页区。
     * <p>
     * 如果所要求的地址不是有效的虚拟内存地址（不属于正在执行的进程的任何一个内存段），则该页不能通过验证，段错误随即产生。
     * 于是，控制权转交给内核的另一部分，通常导致的结果就是进程被强令关闭。
     * <p>
     * 一旦出错的页通过了验证， MMU 随即更新，建立新的虚拟到物理的映射（如有必要，中断被移出页的映射），用户进程得以继续。
     * 造成页错误的用户进程对此不会有丝毫察觉，一切都在不知不觉中进行。
     * <h2>文件I/O</h2>
     * 文件 I/O 属文件系统范畴，文件系统与磁盘迥然不同。磁盘把数据存在扇区上，通常一个扇区
     * 512 字节。磁盘属硬件设备，对何谓文件一无所知，它只是提供了一系列数据存取窗口。在这点
     * 上，磁盘扇区与内存页颇有相似之处：都是统一大小，都可作为大的数组被访问。
     * 文件系统是更高层次的抽象，是安排、解释磁盘（或其他随机存取块设备）数据的一种独特方
     * 式。您所写代码几乎无一例外地要与文件系统打交道，而不是直接与磁盘打交道。是文件系统定义了文件名、路径、文件、文件属性
     * 等抽象概念。
     * <p>
     * 前面讲到，所有 I/O 都是通过请求页面调度完成的。您应该还记得，页面调度是非常底层的
     * 操作，仅发生于磁盘扇区与内存页之间的直接传输。而文件 I/O 则可以任意大小、任意定位。那么，底层的页面调度是如何转换为
     * 文件 I/O 的？
     * <p>
     * 文件系统把一连串大小一致的数据块组织到一起。有些块存储元信息，如空闲块、目录、索引
     * 等的映射，有些包含文件数据。单个文件的元信息描述了哪些块包含文件数据、数据在哪里结束、最后一次更新是什么时候，等等。
     * <p>
     * 当用户进程请求读取文件数据时，文件系统需要确定数据具体在磁盘什么位置，然后着手把相关磁盘扇区读进内存。老式的操作系统
     * 往往直接向磁盘驱动器发布命令，要求其读取所需磁盘扇区。而采用分页技术的现代操作系统则利用请求页面调度取得所需数据。
     * <p>
     * 操作系统还有个页的概念，其大小或者与基本内存页一致，或者是其倍数。典型的操作系统页
     * 从 2,048 到 8,192 字节不等，且始终是基本内存页大小的倍数。采用分页技术的操作系统执行 I/O 的全过程可总结为以下几步：
     * <ul>
     * <li>确定请求的数据分布在文件系统的哪些页（磁盘扇区组）。磁盘上的文件内容和元数据可能跨越多个文件系统页，而且这些页可能也不连续。</li>
     * <li>在内核空间分配足够数量的内存页，以容纳得到确定的文件系统页。</li>
     * <li>在内存页与磁盘上的文件系统页之间建立映射。</li>
     * <li>为每一个内存页产生页错误。</li>
     * <li>虚拟内存系统俘获页错误，安排页面调入，从磁盘上读取页内容，使页有效。</li>
     * <li>一旦页面调入操作完成，文件系统即对原始数据进行解析，取得所需文件内容或属性信息。</li>
     * </ul>
     * <p>
     * 需要注意的是，这些文件系统数据也会同其他内存页一样得到高速缓存。对于随后发生的 I/O请求，文件数据的部分或全部可能仍旧位于
     * 物理内存当中，无需再从磁盘读取即可重复使用。大多数操作系统假设进程会继续读取文件剩余部分，因而会预读额外的文件系统页。如
     * 果内存争用情况不严重，这些文件系统页可能在相当长的时间内继续有效。这样的话，当稍后该文件又被相同或不同的进程再次打开，可
     * 能根本无需访问磁盘。这种情况您可能也碰到过：当重复执行类似的操作，如在几个文件中进行字符串检索，第二遍运行得似乎快多了。
     * <p>
     * 类似的步骤在写文件数据时也会采用。这时，文件内容的改变（通过 write( )）将导致文件系统页变脏，随后通过页面调出，与磁盘上
     * 的文件内容保持同步。文件的创建方式是，先把文件映射到空闲文件系统页，在随后的写操作中，再将文件系统页刷新到磁盘。
     */
    public static void main(String[] args) {

    }

    /**
     * <h2>内存映射文件</h2>
     * 传统的文件 I/O 是通过用户进程发布 read( )和 write( )系统调用来传输数据的。为了在内核空间的文件系统页与用户空间的内存
     * 区之间移动数据，一次以上的拷贝操作几乎总是免不了的。这是因为，在文件系统页与用户缓冲区之间往往没有一一对应关系。但是，还有
     * 一种大多数操作系统都支持的特殊类型的 I/O 操作，允许用户进程最大限度地利用面向页的系统 I/O 特性，并完全摒弃缓冲区拷贝。这
     * 就是内存映射 I/O，如图io7(用户内存到文件系统页的映射)
     * <p>
     * 内存映射 I/O 使用文件系统建立从用户空间直到可用文件系统页的虚拟内存映射。这样做有几个好处：
     *
     * <ul>
     * <li>用户进程把文件数据当作内存，所以无需发布 read( )或 write( )系统调用。</li>
     * <li>当用户进程碰触到映射内存空间，页错误会自动产生，从而将文件数据从磁盘读进内存。如果用户修改了映射内存空间，相关页会自动
     * 标记为脏，随后刷新到磁盘，文件得到更新。</li>
     * <li>操作系统的虚拟内存子系统会对页进行智能高速缓存，自动根据系统负载进行内存管理。</li>
     * <li>数据总是按页对齐的，无需执行缓冲区拷贝。</li>
     * <li>大型文件使用映射，无需耗费大量内存，即可进行数据拷贝。</li>
     * </ul>
     * 虚拟内存和磁盘 I/O 是紧密关联的，从很多方面看来，它们只是同一件事物的两面。在处理大量数据时，尤其要记得这一点。如果数据缓冲
     * 区是按页对齐的，且大小是内建页大小的倍数，那么，对大多数操作系统而言，其处理效率会大幅提升。
     * <h2>文件锁定</h2>
     * <p>
     * 文件锁定机制允许一个进程阻止其他进程存取某文件，或限制其存取方式。通常的用途是控制共享信息的更新方式，或用于事务隔离。在控制多
     * 个实体并行访问共同资源方面，文件锁定是必不可少的。数据库等复杂应用严重信赖于文件锁定。
     * <p>
     * “文件锁定”从字面上看有锁定整个文件的意思（通常的确是那样），但锁定往往可以发生在更为细微的层面，锁定区域往往可以细致到单个字节。
     * 锁定与特定文件相关，开始于文件的某个特定字节地址，包含特定数量的连续字节。这对于协调多个进程互不影响地访问文件不同区域，是至关重
     * 要的。
     * <p>
     * 文件锁定有两种方式：共享的和独占的。多个共享锁可同时对同一文件区域发生作用；独占锁则不同，它要求相关区域不能有其他锁定在起作用。
     * <p>
     * 共享锁和独占锁的经典应用，是控制最初用于读取的共享文件的更新。某个进程要读取文件，会先取得该文件或该文件部分区域的共享锁。第二个
     * 希望读取相同文件区域的进程也会请求共享锁。两个进程可以并行读取，互不影响。但是，假如有第三个进程要更新该文件，它会请求独占锁。该
     * 进程会处于阻滞状态，直到既有锁定（共享的、独占的）全部解除。一旦给予独占锁，其他共享锁的读取进程会处于阻滞状态，直到独占锁解除。
     * 这样，更新进程可以更改文件，而其他读取进程不会因为文件的更改得到前后不一致的结果。图 1-7 和图 1-8 描述了这一过程。
     * <p>
     * 共享锁阻断独占锁请求io8
     * <p>
     * 独占锁阻断共享锁请求io9
     * <p>
     * 文件锁有建议使用和强制使用之分。建议型文件锁会向提出请求的进程提供当前锁定信息，但操作系统并不要求一定这样做，而是由相关进程进行
     * 协调并关注锁定信息。多数 Unix 和类Unix 操作系统使用建议型锁，有些也使用强制型锁或兼而有之。
     * <p>
     * 强制型锁由操作系统或文件系统强行实施，不管进程对锁的存在知道与否，都会阻止其对文件
     * 锁定区域的访问。微软的操作系统往往使用的是强制型锁。假定所有文件锁均为建议型，并在访问共同资源的各个应用程序间使用一致的文件锁定，
     * 是明智之举，也是唯一可行的跨平台策略。依赖于强制文件锁定的应用程序，从根子上讲就是不可移植的。
     *
     * <h2>流I/O</h2>
     * <p>
     * 并非所有 I/O 都像前面讲的是面向块的，也有流 I/O，其原理模仿了通道。 I/O 字节流必须顺序存取，常见的例子有 TTY（控制台）设备、
     * 打印机端口和网络连接。
     * <p>
     * 流的传输一般（也不必然如此）比块设备慢，经常用于间歇性输入。多数操作系统允许把流置于非块模式，这样，进程可以查看流上是否有输入，
     * 即便当时没有也不影响它干别的。这样一种能力使得进程可以在有输入的时候进行处理， 输入流闲置的时候执行其他功能。
     * <p>
     * 比非块模式再进一步，就是就绪性选择。就绪性选择与非块模式类似（常常就是建立在非块模
     * 式之上），但是把查看流是否就绪的任务交给了操作系统。操作系统受命查看一系列流，并提醒进程哪些流已经就绪。这样，仅仅凭借操作系统返
     * 回的就绪信息，进程就可以使用相同代码和单一线程，实现多活动流的多路传输。这一技术广泛用于网络服务器领域，用来处理数量庞大的网络连
     * 接。就绪性选择在大容量缩放方面是必不可少的。
     */
    void memoryMapping() {

    }

    @Test
    public void runs() throws InterruptedException {
        for (int i = 0; i < 10; i++) {
            Thread thread = new Thread() {
                @Override
                public void run() {
                    //在将来的某个时间会执行
                    System.out.println(Thread.currentThread().getName() + "执行了");
                }
            };
            thread.setName("线程" + i);
            thread.start();
        }
        Thread.sleep(10000);
    }

}


